/*
 *  Copyright 2004 Blandware (http://www.blandware.com)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.blandware.atleap.common.util;

import java.io.Serializable;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Locale;
import java.util.Set;
import java.util.HashSet;

/**
 * <p>Contains set of bundles, each contains set of keys mapped to string values</p>
 * <p>Does not accept <code>null</code> values.
 * <p><a href="ApplicationResourceStorage.java.html"><i>View Source</i></a></p>
 *
 * @author Sergey Zubtsovskiy <a href="mailto:sergey.zubtsovskiy@blandware.com">&lt;sergey.zubtsovskiy@blandware.com&gt;</a>
 * @version $Revision: 1.9 $ $Date: 2006/08/03 10:07:32 $
 */
public class ApplicationResourceStorage implements Serializable {

	/**
	 * Stored bundles
	 */
	protected BundleStorage bundles = new BundleStorage();

	/**
	 * Adds message for specified key and locale
	 *
	 * @param locale Locale of message
	 * @param key    Key to associate value with
	 * @param value  Value to associate with key
	 */
	public synchronized void putMessage(Locale locale, String key, String value) {
		if ( value == null ) {
			throw new NullPointerException("Value cannot be null");
		}

		Bundle bundle = bundles.getBundle(locale);
		if ( bundle == null ) {
			bundle = new Bundle(locale);
			bundles.addBundle(bundle);
		}

		bundles.getBundle(locale).putMessage(key, value);
	}

	/**
	 * Returns set of keys stored in bundle associated with specified locale
     *
	 * @param locale Locale of bundle to get keys
	 * @return Set of keys, stored in bundle associated with specified locale
	 */
	public Set getKeys(Locale locale) {
		Bundle bundle = bundles.getBundle(locale);
		Set keys = new HashSet();
		if ( bundle != null ) {
			keys = bundle.getKeys();
		}
		return keys;
	}

	/**
	 * Adds specified bundle to the storage. Replace existing one in storage.
     *
	 * @param bundle Bundle to add to storage
	 */
	public void mergeBundle(Bundle bundle) {
        bundles.addBundle(bundle);
	}

	/**
	 * Removes all messages for specified locale
     *
	 * @param locale Locale to remove messages
	 */
	public void dropBundle(Locale locale) {
		bundles.removeBundle(locale);
	}

	/**
	 * Returns a text message after parametric replacement of the specified parameter placeholders
	 *
	 * @param locale Locale to get message for
	 * @param key    Key to get associated value
	 * @param args   Arguments to put in message
	 * @return Formatted message for specified key and locale
	 */
	public String getMessage(Locale locale, String key, Object[] args) {
		Bundle bundle = bundles.getBundle(locale);
		return bundle != null ? bundle.getMessage(key, args) : null;
	}

	/**
	 * Returns <code>true</code> if message for specified key and locale is present and <code>false</code> otherwise
     *
	 * @param locale Locale to get bundle for
	 * @param key Key to search message
	 * @return whether message is present
	 */
	public boolean isMessagePresent(Locale locale, String key) {
		return getMessage(locale, key, null) != null;
	}

	/**
	 * Represents list of mappings <code>key</code> -&gt; <code>value</code>
	 */
	public static class Bundle implements Serializable {

		/**
		 * Stored messages
		 */
		protected HashMap messages = new HashMap();

		/**
		 * Cached <code>java.text.MessageFormat</code> objects
		 */
		protected HashMap formats = new HashMap();

		/**
		 * Locale associated with this bundle
		 */
		protected Locale locale;

		/**
		 * Creates new instance of bundle
         *
		 * @param locale Locale to associate with bundle
		 */
		public Bundle(Locale locale) {
			if ( locale == null ) {
				throw new IllegalArgumentException("Locale cannot be null");
			}
			this.locale = locale;
		}

		/**
		 * Returns locale of this bundle
         *
		 * @return Locale of this bundle
		 */
		public Locale getLocale() {
			return locale;
		}

		/**
		 * Returns a text message without any parametric replacement of placeholders,
		 * which are probably presented in string
		 *
		 * @param key Key to search message for
		 * @return Value associated with specified key or <code>null</code> if none found
		 */
		public String getMessage(String key) {
			return (String) messages.get(key);
		}

		/**
		 * Returns a text message after parametric replacement of the specified parameter placeholders
		 *
		 * @param key  Key to retrieve message for
		 * @param args Array of arguments
		 * @return Formatted message, or <code>null</code> if none found
		 */
		public String getMessage(String key, Object[] args) {

			if ( args == null || args.length == 0 ) {
				return getMessage(key);
			}

			MessageFormat format = null;
			synchronized ( formats ) {
				format = (MessageFormat) formats.get(key);
				if ( format == null ) {
					String msg = getMessage(key);

					if ( msg == null ) {
	                    return null;
                    }

					format = new MessageFormat(escape(msg));
					format.setLocale(locale);

					formats.put(key, format);
				}
			}

			return format.format(args);
		}

		/**
		 * Returns set of keys stored in this bundle
         *
		 * @return Set of keys
		 */
		public Set getKeys() {
			return messages.keySet();
		}

		/**
		 * Adds new key-value pair to the bundle
		 *
		 * @param key   Key to associate value with
		 * @param value Value to associate with key
		 */
		public void putMessage(String key, String value) {
			messages.put(key, value);
			
			// flush format associated with this key if any
			formats.remove(key);
		}

		/**
		 * Returns number of stored messages
		 *
		 * @return Number of stored messages
		 */
		public int getSize() {
			return messages.size();
		}

		/**
		 * Escapes any single quote characters in string
		 *
		 * @param string The string to escape single quote characters in
         * @return escaped string
		 */
		protected String escape(String string) {

			if ( (string == null) || (string.indexOf('\'') < 0) ) {
				return string;
			}

			int n = string.length();
			StringBuffer sb = new StringBuffer(n);

			for ( int i = 0; i < n; i++ ) {
				char ch = string.charAt(i);

				if ( ch == '\'' ) {
					sb.append('\'');
				}

				sb.append(ch);
			}

			return sb.toString();

		}
	}

	/**
	 * Stores bundles for different locales
	 */
	protected static class BundleStorage implements Serializable {

		/**
		 * Stored bundles
		 */
		protected HashMap bundles = new HashMap();

		/**
		 * Adds new bundle to storage and associates it with corresponding locale
		 *
		 * @param bundle Bundle to add to storage
		 */
		public void addBundle(Bundle bundle) {
			Locale locale = bundle.getLocale();
			String localeKey = getLocaleKey(locale);
			bundles.put(localeKey, bundle);
		}

		/**
		 * Removes bundle from storage
         *
		 * @param locale Locale of bundle to remove
		 */
		public void removeBundle(Locale locale) {
			bundles.remove(getLocaleKey(locale));
		}

		/**
		 * Returns bundle associated with specified locale
		 *
		 * @param locale Locale to search bundles for
		 * @return Bundle associated with specified locale
		 */
		public Bundle getBundle(Locale locale) {
			return (Bundle) bundles.get(getLocaleKey(locale));
		}

		/**
		 * Returns key representing specified locale to use in map of bundles
		 *
		 * @param locale Locale to get key for
		 * @return Key representing specified locale to use in map of bundles
		 */
		protected String getLocaleKey(Locale locale) {
			return locale.toString();
		}
	}

}
